package wx;

import constant.Request;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.socket.ConnectionSocketFactory;
import org.apache.http.conn.socket.PlainConnectionSocketFactory;
import org.apache.http.conn.ssl.DefaultHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
import org.apache.http.util.EntityUtils;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import util.HttpUtil;
import util.JsonUtil;
import util.Md5Util;
import util.StringUtil;
import util.XmlUtil;
import wx.pojo.PayConfig;
import wx.request.PayInfo;
import wx.request.RefundInfo;
import wx.request.RefundQueryInfo;
import wx.response.RefundOrderResponse;
import wx.response.RefundQueryResponse;
import wx.response.UnifiedOrderResponse;

import javax.crypto.Cipher;
import javax.crypto.spec.SecretKeySpec;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import java.io.FileInputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.security.Security;
import java.util.Base64;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * 微信支付相关接口
 *
 * @author zhanglinfeng
 * @date create in 2023/7/18 11:29
 */
public class WXPayUtil {
    //将BC方法添加进环境信息,用于解密微信退款回调信息
    static {
        Security.addProvider(new BouncyCastleProvider());
    }

    private final String URL_UNIFIED_ORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";
    private final String URL_REFUND_ORDER = "https://api.mch.weixin.qq.com/secapi/pay/refund";
    private final String URL_REFUND_QUERY = "https://api.mch.weixin.qq.com/pay/refundquery";
    private final String USER_AGENT = "WXPaySDK/3.0.9 (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
            ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();

    public Logger LOGGER = LoggerFactory.getLogger(WXPayUtil.class);

    /**
     * 微信支付
     *
     * @param payConfig 配置信息
     * @param openId    用户openid
     * @param clientIP  请求IP
     * @param body      商品描述
     * @param attach    附加数据
     * @param tradeNo   商户订单号
     * @param totalFee  标价金额，单位分
     * @return UnifiedOrderResponse
     * @throws Exception 异常
     */
    public UnifiedOrderResponse unifiedOrder(PayConfig payConfig, String openId, String clientIP, String body, String attach, String tradeNo, String totalFee) throws Exception {
        String randomNonceStr = StringUtil.randomCode(32, true);
        PayInfo payInfo = new PayInfo(payConfig.getWxAppId(), payConfig.getWxMchId(), payConfig.getOrderCallbackUrl(), openId, clientIP, randomNonceStr, body, attach, tradeNo, totalFee);
        try {
            String xml = dealSignAndReturnXml(payInfo, payConfig.getWxMchKey());
            String result = HttpUtil.httpsRequest(URL_UNIFIED_ORDER, Request.POST, xml);
            return XmlUtil.toObject(result, UnifiedOrderResponse.class);
        } catch (Exception e) {
            LOGGER.error("微信统一下单异常，payInfo：{}", JsonUtil.toString(payInfo), e);
            throw new Exception("统一下单异常");
        }
    }

    /**
     * 微信退款
     *
     * @param payConfig     配置信息
     * @param transactionId 微信支付订单号
     * @param outRefundNo   商户退款单号
     * @param totalFee      订单金额，单位分
     * @param refundFee     退款金额，单位分
     * @param refundDesc    退款原因
     * @return RefundOrderResponse
     * @throws Exception 异常
     */
    public RefundOrderResponse refundOrder(PayConfig payConfig, String transactionId, String outRefundNo, int totalFee, int refundFee, String refundDesc) throws Exception {
        if (StringUtil.isEmpty(payConfig.getWxMchPath())) {
            throw new Exception("商户证书不存在");
        }
        String randomNonceStr = StringUtil.randomCode(32, true);
        RefundInfo refundInfo = new RefundInfo(payConfig.getWxAppId(), payConfig.getWxMchId(), randomNonceStr, transactionId, outRefundNo, totalFee, refundFee, refundDesc, payConfig.getRefundCallbackUrl());
        try {
            //生成签名并返回xml
            String xml = dealSignAndReturnXml(refundInfo, payConfig.getWxMchKey());
            String result = this.requestWithCert(URL_REFUND_ORDER, xml, payConfig.getWxMchId(), payConfig.getWxMchPath());
            return XmlUtil.toObject(result, RefundOrderResponse.class);
        } catch (Exception e) {
            LOGGER.error("微信申请退款异常，transactionId：{},refundInfo：{}", transactionId, JsonUtil.toString(refundInfo), e);
            throw new Exception("退款异常");
        }
    }

    /**
     * 查询退款
     *
     * @param payConfig     配置信息
     * @param transactionId 微信支付订单号
     * @return RefundQueryResponse
     * @throws Exception 异常
     */
    public RefundQueryResponse refundQuery(PayConfig payConfig, String transactionId) throws Exception {
        String randomNonceStr = StringUtil.randomCode(32, true);
        RefundQueryInfo refundQueryInfo = new RefundQueryInfo(payConfig.getWxAppId(), payConfig.getWxMchId(), randomNonceStr, transactionId);
        try {
            String xml = dealSignAndReturnXml(refundQueryInfo, payConfig.getWxMchKey());
            String result = HttpUtil.httpsRequest(URL_REFUND_QUERY, Request.POST, xml);
            return XmlUtil.toObject(result, RefundQueryResponse.class);

        } catch (Exception e) {
            LOGGER.error("微信查询退款异常，payInfo：{}", JsonUtil.toString(refundQueryInfo), e);
            throw new Exception("查询退款异常");
        }
    }

    /**
     * 解密返回数据
     *
     * @param payConfig 配置
     * @param reqInfo   返回数据
     * @return Map
     * @throws Exception 异常
     */
    public static Map<String, String> decodeReqInfo(PayConfig payConfig, String reqInfo) throws Exception {
        //对加密串A做base64解码，得到加密串B
        byte[] reqInfoByte = Base64.getDecoder().decode(reqInfo);
        //对商户key做md5，得到32位小写key*
        String wxMchKey = payConfig.getWxMchKey();
        String md5WxMchKey = Md5Util.md5Encode(wxMchKey).toLowerCase();
        //用key*对加密串B做AES-256-ECB解密（PKCS7Padding）
        Cipher cipher = Cipher.getInstance("AES/ECB/PKCS7Padding", "BC");
        SecretKeySpec secretKeySpec = new SecretKeySpec(md5WxMchKey.getBytes(), "AES");
        cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
        return XmlUtil.parseXml(new String(cipher.doFinal(reqInfoByte)));
    }

    /**
     * 需要证书的请求
     *
     * @param url       请求地址
     * @param data      请求数据
     * @param wxMchId   商户ID
     * @param wxMchPath 证书地址
     * @return 微信返回数据
     * @throws Exception 异常
     */
    public String requestWithCert(String url, String data, String wxMchId, String wxMchPath) throws Exception {
        char[] password = wxMchId.toCharArray();
        // 证书
        FileInputStream inStream = new FileInputStream(wxMchPath);
        KeyStore ks = KeyStore.getInstance("PKCS12");
        ks.load(inStream, password);
        // 实例化密钥库 & 初始化密钥工厂
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, password);
        // 创建 SSLContext
        SSLContext sslContext = SSLContext.getInstance("TLS");
        sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
        SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(sslContext, null, null, new DefaultHostnameVerifier());
        BasicHttpClientConnectionManager connManager = new BasicHttpClientConnectionManager(RegistryBuilder.<ConnectionSocketFactory>create()
                .register("http", PlainConnectionSocketFactory.getSocketFactory()).register("https", sslConnectionSocketFactory).build(), null, null, null);
        HttpClient httpClient = HttpClientBuilder.create().setConnectionManager(connManager).build();

        HttpPost httpPost = new HttpPost(url);
        RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(3000).setConnectTimeout(3000).build();
        httpPost.setConfig(requestConfig);
        StringEntity postEntity = new StringEntity(data, "UTF-8");
        httpPost.addHeader("Content-Type", "text/xml");
        httpPost.addHeader("User-Agent", USER_AGENT + " " + wxMchId);
        httpPost.setEntity(postEntity);
        HttpResponse httpResponse = httpClient.execute(httpPost);
        HttpEntity httpEntity = httpResponse.getEntity();
        return EntityUtils.toString(httpEntity, "UTF-8");
    }

    /**
     * 请求内容生成签名并转xml格式
     *
     * @param obj      请求对象
     * @param wxMchKey 商户秘钥
     * @return xml格式
     * @throws IllegalAccessException 异常
     */
    private static String dealSignAndReturnXml(Object obj, String wxMchKey) throws Exception {
        StringBuilder sb = new StringBuilder("<xml>");
        //对象转key=value的格式，并按照参数名ASCII字典序排序
        Map<String, String> objMap = new HashMap<>();
        Class<?> objClass = obj.getClass();
        Field[] fields = objClass.getDeclaredFields();
        for (Field field : fields) {
            field.setAccessible(true);
            if (null == field.get(obj)) {
                continue;
            }
            objMap.put(field.getName(), field.get(obj).toString());
            sb.append("<").append(field.getName()).append(">").append(field.get(obj).toString()).append("</").append(field.getName()).append(">");
        }
        //生成签名
        String signStr = objMap.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(m -> m.getKey() + "=" + m.getValue()).collect(Collectors.joining("&"));
        //对象转xml格式
        sb.append("<sign>").append(Md5Util.md5Encode(signStr + "&key=" + wxMchKey).toUpperCase()).append("</sign>").append("</xml>");
        return sb.toString();
    }

    public static boolean checkSign(Map<String, String> map, String wxMchKey) {
        String checkSign = map.get("sign");
        //生成签名
        String signStr = map.entrySet().stream().filter(t -> {
            if (StringUtil.isEmpty(t.getValue())) {
                return false;
            }
            return !"sign".equals(t.getKey()) && !"sign_type".equals(t.getKey());
        }).sorted(Map.Entry.comparingByKey()).map(m -> m.getKey() + "=" + m.getValue()).collect(Collectors.joining("&"));
        signStr = signStr + "&key=" + wxMchKey;
        String sign = DigestUtils.md5Hex(signStr.getBytes(StandardCharsets.UTF_8)).toUpperCase();;
        return checkSign.equals(sign);
    }

}
